feat(security): add vault-anchor downgrade resistance for transcript integrity#6461
Merged
Merged
Conversation
…integrity Closes #6449 A file-write-only attacker could strip every chain field from a subagent transcript or session log JSONL file, or delete a durable execution's integrity row entirely, making tampered content indistinguishable from genuine pre-feature legacy content that is auto-trusted by design (#6360's stated residual). Adds a per-file vault anchor for the JSONL surfaces: a small {epoch, count, head, written_at} attestation mirrored to the age vault on finalize and checked on read. Since the vault cannot be selectively edited without the age key, a whole-file strip now leaves a detectable mismatch between the file and its anchor. A bounded reconcile-and-cap sweep reaps orphaned anchors and caps vault growth via LRU eviction keyed on the anchor's own embedded write timestamp, not filesystem mtime, so eviction order cannot be manipulated by a file-write attacker. For durable executions, replaces the "missing integrity row is legacy" trust assumption with an operator-gated vault seal: once sealed, an absent integrity row on a keyed, drained execution is unconditional tamper. The seal boundary is entirely vault-resolved so no attacker-writable database column participates in the tamper decision. Config gains `[integrity]` (anchor mode, session-anchor cap); new CLI `durable seal-integrity` and `sessions verify`; wired into doctor, --init, --migrate-config, and the TUI command palette. Accepted residuals (session-prefix rollback, grandfather opt-out, reconcile-window recreation) are documented rather than silently left implicit.
This was referenced Jul 18, 2026
bug-ops
enabled auto-merge (squash)
July 18, 2026 17:33
bug-ops
added a commit
that referenced
this pull request
Jul 18, 2026
* docs(specs): sync specs/ with commits merged since v0.22.1 Covers the durable-execution key-rotation surface (#6447, #6451, #6460), subagent live transcript forwarding (#6359/#6455), and web_search follow-ups (#6457): - specs/064-durable-execution: document the previously-unspecified `zeph durable rotate-key`/`--drop-previous` CLI, the unified AEAD/ control-HMAC/HWM key_id/previous_key_id rotation lifecycle, the three drop-previous safety scans, and the durable_execution_integrity HWM table/folded_count column. - specs/081-transcript-integrity: add addendum documenting the HWM key-rotation window fix (issue #6460) and correct stale README/MOC summaries that still described the whole-file downgrade gap as open after #6461 closed it. - specs/044-subagent-lifecycle: add new section documenting opt-in live transcript forwarding (config, mechanism, invariants); cross-reference from specs/026-tui-subagent-management and specs/047-cli-modes. - specs/006-tools/006-1-web-search: document resolved-address-set client caching and real backend-status propagation into egress telemetry. - specs/README.md, specs/MOC-specs.md: refresh index summaries for all of the above. * release: prepare v0.22.2 Bump version from 0.22.1 to 0.22.2, finalize the changelog entry, and sync generated documentation (mdBook pages, crate READMEs, root README) with the features and fixes merged since v0.22.1.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #6449
The base transcript-integrity feature (#6360, PR #6453) shipped hash-chain
tamper-evidence for subagent transcripts, session logs, and the durable
journal, but explicitly deferred defense against a fully-consistent
whole-file/whole-execution downgrade: an attacker who deletes every chain
field from a JSONL file (or the durable integrity row entirely) makes
tampered content indistinguishable from genuine pre-feature legacy content,
which is auto-trusted by design.
This PR closes that gap:
{epoch, count, head, written_at}is written to the age vault on finalizeand checked on read. Since the vault cannot be selectively edited without
the age key, a whole-file strip now produces a detectable mismatch. A
bounded reconcile-and-cap sweep reaps orphaned anchors and caps vault
growth via LRU eviction keyed on the anchor's own embedded write
timestamp (not filesystem mtime, which is attacker-writable).
with an operator-gated vault seal (
zeph durable seal-integrity). Oncesealed, an absent integrity row on a keyed, drained execution is
unconditional tamper. The seal boundary is entirely vault-resolved — no
attacker-writable database column participates in the tamper decision.
[integrity]config section,zeph sessions verifyCLI, doctor/--init/--migrate-config/TUI integration.(unanchored tail truncation), grandfather opt-out (permanent forgeable
slot by design), reconcile-window recreation (bounded, requires a
destructive precursor).
Design process
Went through 2 architecture revisions and 2 rounds of adversarial critique
before implementation:
created_at-based floor; critic found it wasdefeated by the same DB-write threat model it targeted (the comparison
column was unauthenticated and attacker-writable).
drain-before-seal precondition — critic confirmed this closed the gap,
but found the session-anchor LRU eviction picked victims by attacker-
writable filesystem mtime (same flaw class). Fixed to evict by an
AEAD-protected embedded timestamp instead.
Implementation was independently validated by parallel test/perf/security/
critic passes, which surfaced and fixed: an insta snapshot regression, a
non-functional read-timeout (the
tokio::time::timeoutwrapped analready-resolved future), a vault write-lock held across synchronous
filesystem stats, an inaccurately-documented reconcile-reap residual, and a
dropped NFR-005 audit-event requirement. Final review required two more
rebases as
origin/mainadvanced during the cycle (#6455, #6457, #6460),including a careful reconciliation in
zeph-durable'slocal.rsto confirma concurrently-merged HMAC-rotation feature did not reintroduce a
database-derived value onto the tamper-decision boundary.
Test plan
fmt --check,clippy --profile ci(full feature set), rustdoc gate,
cargo test --doccargo nextest run— 14607 passed, 0 failed, 35 skippedgitleaks protect --staged— cleanchained file (no anchor) → still opens (no migration break)
created_at+ deleted integrity row on a post-sealkeyed execution → still TAMPER (proves no DB column on the boundary)
written_at, not filesystem mtime, under deliberately conflictingtest setup
contention (not mocked) for both the async and sync anchor-lookup
paths
.local/testing/)